
using System.Collections;
using System;

using System.IO;
using Velocity4Net.Runtime;
using Velocity4Net.Runtime.Parse.AST;
using Velocity4Net.Runtime.Parse;
using Velocity4Net.Errors;
namespace Velocity4Net.Ctx
{
    /**
     * Context for Velocity macro arguments.
     * 
     * This special context combines ideas of earlier VMContext and VMProxyArgs
     * by implementing routing functionality internally. This significantly
     * reduces memory allocation upon macro invocations.
     * Since the macro AST is now shared and RuntimeMacro directive is used,
     * the earlier implementation of precalculating VMProxyArgs would not work.
     * 
     * See <a href="http://issues.apache.org/jira/browse/VELOCITY-607">Issue 607</a>
     * for more info on this class.
     * @author <a href="mailto:wyla@removeme.sci.fi">Jarkko Viinamaki</a>
     * @version $Id$
     * @since 1.6
     */
    public class ProxyVMContext : ChainedInternalContextAdapter
    {
        /** container for our macro AST node arguments. Size must be power of 2. */
        Hashtable vmproxyhash = new Hashtable(8, 0.8f);

        /** container for any local or constant macro arguments. Size must be power of 2. */
        Hashtable localcontext = new Hashtable(8, 0.8f);

        /** support for local context scope feature, where all references are local */
        private bool localContextScope;

        /** needed for writing log entries. */
        private RuntimeServices rsvc;

        /**
         * @param inner Velocity context for processing
         * @param rsvc RuntimeServices provides logging reference
         * @param localContextScope if true, all references are set to be local
         */
        public ProxyVMContext(InternalContextAdapter inner,
                              RuntimeServices rsvc,
                              bool localContextScope)
            : base(inner)
        {
            this.localContextScope = localContextScope;
            this.rsvc = rsvc;
        }

        /**
         * Used to put Velocity macro arguments into this context. 
         * 
         * @param context rendering context
         * @param macroArgumentName name of the macro argument that we received
         * @param literalMacroArgumentName ".literal.$"+macroArgumentName
         * @param argumentValue actual value of the macro argument
         * 
         * @throws MethodInvocationException
         */
        public void addVMProxyArg(InternalContextAdapter context,
                                  String macroArgumentName,
                                  String literalMacroArgumentName,
                                  INode argumentValue)
        {
            if (isConstant(argumentValue))
            {
                localcontext.Add(macroArgumentName, argumentValue.GetValue(context));
            }
            else
            {
                vmproxyhash.Add(macroArgumentName, argumentValue);
                localcontext.Add(literalMacroArgumentName, argumentValue);
            }
        }

        /**
         * Used to put Velocity macro bodyContext arguments into this context. 
         * 
         * @param context rendering context
         * @param macroArgumentName name of the macro argument that we received
         * @param literalMacroArgumentName ".literal.$"+macroArgumentName
         * @param argumentValue actual value of the macro body
         * 
         * @throws MethodInvocationException
         */
        public void addVMProxyArg(InternalContextAdapter context,
                                  String macroArgumentName,
                                  String literalMacroArgumentName,
                                  Renderable argumentValue)
        {
            localcontext.Add(macroArgumentName, argumentValue);
        }

        /**
         * AST nodes that are considered constants can be directly
         * saved into the context. Dynamic values are stored in
         * another argument hashmap.
         * 
         * @param node macro argument as AST node
         * @return true if the node is a constant value
         */
        private bool isConstant(INode node)
        {
            switch (node.NodeType)
            {
                case ParserTreeConstants.JJTINTEGERRANGE:
                case ParserTreeConstants.JJTREFERENCE:
                case ParserTreeConstants.JJTOBJECTARRAY:
                case ParserTreeConstants.JJTMAP:
                case ParserTreeConstants.JJTSTRINGLITERAL:
                case ParserTreeConstants.JJTTEXT:
                    return (false);
                default:
                    return (true);
            }
        }

        /**
         * Impl of the Context.put() method.
         * 
         * @param key name of item to set
         * @param value object to set to key
         * @return old stored object
         */
        public Object put(String key, Object value)
        {
            return put(key, value, localContextScope);
        }

        /**
         * Allows callers to explicitly put objects in the local context, no matter what the
         * velocimacro.context.local setting says. Needed e.g. for loop variables in foreach.
         * 
         * @param key name of item to set.
         * @param value object to set to key.
         * @return old stored object
         */
        public Object localPut(String key, Object value)
        {
            return put(key, value, true);
        }

        /**
         * Internal put method to select between local and global scope.
         * 
         * @param key name of item to set
         * @param value object to set to key
         * @param forceLocal True forces the object into the local scope.
         * @return old stored object
         */
        protected Object put(String key, Object value, bool forceLocal)
        {
            Object old = value;
            localcontext.Add(key, value);
            if (!forceLocal)
            {
                old = base.Put(key, value);
            }
            return old;
        }

        /**
         * Implementation of the Context.get() method.  First checks
         * localcontext, then arguments, then global context.
         * 
         * @param key name of item to get
         * @return stored object or null
         */
        public Object get(String key)
        {
            Object o = localcontext[key];
            if (o != null)
            {
                return o;
            }

            INode astNode = (INode)vmproxyhash[key];

            if (astNode != null)
            {
                int type = astNode.NodeType;

                // if the macro argument (astNode) is a reference, we need to evaluate it
                // in case it is a multilevel node
                if (type == ParserTreeConstants.JJTREFERENCE)
                {
                    ASTReference _ref = (ASTReference)astNode;

                    if (_ref.jjtGetNumChildren() > 0)
                    {
                        return _ref.Execute(null, innerContext);
                    }
                    else
                    {
                        Object obj = innerContext.Get(_ref.RootString);
                        if (obj == null && _ref.strictRef)
                        {
                            if (!innerContext.ContainsKey(_ref.RootString))
                            {
                                throw new MethodInvocationException("Parameter '" + _ref.RootString
                                    + "' not defined", null, key, _ref.TemplateName,
                                    _ref.Line, _ref.Column);
                            }
                        }
                        return obj;
                    }
                }
                else if (type == ParserTreeConstants.JJTTEXT)
                {
                    // this really shouldn't happen. text is just a throwaway arg for #foreach()
                    try
                    {
                        StringWriter writer = new StringWriter();
                        astNode.Render(innerContext, writer);
                        return writer.ToString();
                    }
                    catch (ApplicationException e)
                    {
                        throw e;
                    }
                    catch (System.Exception e)
                    {
                        String msg = "ProxyVMContext.get() : error rendering reference";
                        rsvc.getLog().error(msg, e);
                        throw new VelocityException(msg, e);
                    }
                }
                else
                {
                    // use value method to render other dynamic nodes
                    return astNode.GetValue(innerContext);
                }
            }

            return base.Get(key);
        }

        /**
         * @see org.apache.velocity.context.Context#containsKey(java.lang.Object)
         */
        public bool ContainsKey(Object key)
        {
            return vmproxyhash.ContainsKey(key)
                || localcontext.ContainsKey(key)
                || base.ContainsKey(key);
        }

        /**
         * @see org.apache.velocity.context.Context#getKeys()
         */
        public Object[] getKeys()
        {
            return null;
            //if (localcontext.Count==0)
            //{
            //    return vmproxyhash.Keys;
            //}
            //else if (vmproxyhash.Count==0)
            //{
            //    return localcontext.Keys;
            //}

            //ICollection keys = new ArrayList(localcontext.Keys);
            //keys.a(vmproxyhash.keySet());
            //return keys.toArray();
        }

        /**
         * @see org.apache.velocity.context.Context#remove(java.lang.Object)
         */
        public Object remove(Object key)
        {
            Object loc = localcontext[key];
            localcontext.Remove(key);
            Object glo = null;

            vmproxyhash.Remove(key);

            if (!localContextScope)
            {
                glo = base.Remove(key);
            }
            if (loc != null)
            {
                return loc;
            }
            return glo;
        }

    }
}